﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;

namespace Wheels
{
    [Obsolete("Don't use until fixed.", true)]
    public static class ComponentEmitUtility
    {
#pragma warning disable 1591

        private static readonly Type smINotifyPropertyChangedType = typeof(INotifyPropertyChanged);
        private static readonly Type smPropertyChangedEventHandler = typeof(PropertyChangedEventHandler);
        private static readonly Type smPropertyChangedEventArgs = typeof(PropertyChangedEventArgs);
        private static readonly EventInfo smPropertyChangedEvent;
        private static MethodInfo smAddPropertyChangedMethod;
        private static MethodInfo smRemovePropertyChangedMethod;
        private static MethodInfo smInvokePropertyChangedMethod;

        private const MethodAttributes RaisePropertyChangedMethodAttributes = MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final;
        private static readonly ConstructorInfo smPropertyChangedEventArgsConstructor;

        private const BindingFlags NonPublicInstanceMemberBindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
        private const TypeAttributes NewTypeAttributes = TypeAttributes.AutoLayout | TypeAttributes.Public | TypeAttributes.Class;
        private const BindingFlags PublicInstanceMemberBindingFlags = BindingFlags.Public | BindingFlags.Instance;
        private const BindingFlags InheritableConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

        static ComponentEmitUtility()
        {
            smPropertyChangedEvent = smINotifyPropertyChangedType.GetEvent("PropertyChanged");
            smAddPropertyChangedMethod = smPropertyChangedEvent.GetAddMethod();
            smRemovePropertyChangedMethod = smPropertyChangedEvent.GetRemoveMethod();
            smInvokePropertyChangedMethod = smPropertyChangedEventHandler.GetMethod("Invoke");

            smPropertyChangedEventArgsConstructor = smPropertyChangedEventArgs.GetConstructor(new[] { Types.String });
        }

        public static MethodInfo EnsureRaisePropertyChangedImplemented(this TypeBuilder typeBuilder, string methodName = "RaisePropertyChanged")
        {
            if (typeBuilder == null)
                throw NewException.ArgumentNull(() => typeBuilder);
            if (methodName.IsNullOrWhiteSpace())
                throw NewException.ArgumentOutOfRange(() => methodName);

            var method = typeBuilder.BaseType.GetMethod(methodName, NonPublicInstanceMemberBindingFlags, null, new[] { Types.String }, null);
            if (method != null)
                return method;

            var field = typeBuilder.EnsureINotifyPropertyChangedImplemented();

            // TODO: replace with a better validating
            if (methodName.IndexOfAny(new[] { '.', '<', '>', '_' }) < 0)
                methodName = smINotifyPropertyChangedType.Name + "." + methodName;

            var builder = typeBuilder.DefineMethod(methodName, RaisePropertyChangedMethodAttributes, Types.Void, new[] { smPropertyChangedEventArgs });
            var il = builder.GetILGenerator();
            il.DeclareLocal(smPropertyChangedEventHandler);
            var label = il.DefineLabel();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, field);
            il.Emit(OpCodes.Stloc_0);

            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ldnull);
            il.Emit(OpCodes.Ceq);
            il.Emit(OpCodes.Brtrue, label);

            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Newobj, smPropertyChangedEventArgsConstructor);
            il.Emit(OpCodes.Call, smInvokePropertyChangedMethod);

            il.MarkLabel(label);
            il.Emit(OpCodes.Ret);

            return builder;
        }
        public static FieldInfo EnsureINotifyPropertyChangedImplemented(this TypeBuilder typeBuilder, string eventName = "PropertyChanged")
        {
            if (typeBuilder == null)
                throw NewException.ArgumentNull(() => typeBuilder);
            if (eventName.IsNullOrWhiteSpace())
                throw NewException.ArgumentOutOfRange(() => eventName);

            if (typeBuilder.BaseType.GetInterfaces().Contains(smINotifyPropertyChangedType))
            {
                var field = typeBuilder.BaseType.GetField(eventName, NonPublicInstanceMemberBindingFlags);
                if (field != null) return field;
            }

            // TODO: replace with a better validating
            if (eventName.IndexOfAny(new[] { '.', '<', '>', '_' }) < 0)
                eventName = smINotifyPropertyChangedType.Name + "." + eventName;

            typeBuilder.AddInterfaceImplementation(smINotifyPropertyChangedType);

            var fieldBuilder = typeBuilder.DefineField(eventName, smPropertyChangedEventHandler, FieldAttributes.Private);
            var builder = typeBuilder.DefineEvent(eventName, EventAttributes.None, smPropertyChangedEventHandler);

            var addAccessor = typeBuilder.CreateEventAccessor(fieldBuilder, EventAccessorType.Add);
            typeBuilder.DefineMethodOverride(addAccessor, smAddPropertyChangedMethod);
            builder.SetAddOnMethod(addAccessor);

            var removeAccessor = typeBuilder.CreateEventAccessor(fieldBuilder, EventAccessorType.Remove);
            typeBuilder.DefineMethodOverride(removeAccessor, smRemovePropertyChangedMethod);
            builder.SetRemoveOnMethod(removeAccessor);

            return fieldBuilder;
        }

        public static Type MakeComponentType(this ModuleBuilder moduleBuilder, string typeName, Type baseType)
        {
            if (moduleBuilder == null)
                throw NewException.ArgumentNull(() => moduleBuilder);
            if (typeName.IsNullOrWhiteSpace())
                throw NewException.ArgumentOutOfRange(() => typeName);
            if (baseType == null)
                throw NewException.ArgumentNull(() => baseType);
            if (!baseType.IsSealed)
                throw NewException.Argument(() => baseType);

            var builder = moduleBuilder.DefineType(typeName, NewTypeAttributes, baseType);

            var raisePropertyChangedMethod = builder.EnsureRaisePropertyChangedImplemented();

            foreach (var prop in baseType.GetProperties(PublicInstanceMemberBindingFlags).Where(p => p.CanWrite))
            {
                builder.ImplementPropertyChangedNotification(raisePropertyChangedMethod, prop);
            }

            foreach (var ctor in baseType.GetConstructors(InheritableConstructorBindingFlags))
            {
                switch (ctor.Attributes & MethodAttributes.MemberAccessMask)
                {
                    case MethodAttributes.Family:
                    case MethodAttributes.Public:
                    case MethodAttributes.FamORAssem:
                        builder.ImplementConstructor(ctor);
                        break;
                }
            }

            var resultType = builder.CreateType();
            return resultType;
        }

        public static void ImplementPropertyChangedNotification(this TypeBuilder typeBuilder, MethodInfo raisePropertyChangedMethod, PropertyInfo property)
        {
            if (typeBuilder == null)
                throw NewException.ArgumentNull(() => typeBuilder);
            if (raisePropertyChangedMethod == null)
                throw NewException.ArgumentNull(() => raisePropertyChangedMethod);
            if (property == null)
                throw NewException.ArgumentNull(() => property);

            var getter = property.GetGetMethod();
            if (getter == null)
                throw NewException.Argument(() => property);

            var setter = property.GetSetMethod();
            if (setter == null || !setter.IsVirtual)
                throw NewException.Argument(() => property);

            var parameters = setter.GetParameters();
            var paramTypes = parameters.Select(p => p.ParameterType).ToArray();

            var builder = typeBuilder.DefineMethod(setter.Name, setter.Attributes, setter.ReturnType, paramTypes);

            // exclude "this"
            for (int i = 0; i < parameters.Length; i++)
            {
                var param = parameters[i];
                builder.DefineParameter(i + 1, param.Attributes, param.Name);
            }

            var il = builder.GetILGenerator();

            var label = il.DefineLabel();

            il.Emit(OpCodes.Ldarg_1);

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Call, getter);

            il.Emit(OpCodes.Ceq);
            il.Emit(OpCodes.Brtrue, label);

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Call, setter);

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldstr, property.Name);
            il.Emit(OpCodes.Call, raisePropertyChangedMethod);

            il.MarkLabel(label);
            il.Emit(OpCodes.Ret);

            typeBuilder.DefineMethodOverride(builder, setter);
        }
    }
}
